#!/usr/bin/env python

try:
    from collections import OrderedDict
except ImportError:
    from OrderedDict import OrderedDict
from collections import defaultdict
from glob import iglob
from itertools import chain
from os import listdir
from os.path import *
from shlex import split
from subprocess import list2cmdline, Popen, PIPE
from sys import platform

# notes about doing things manually with makefiles:
# you _must_ get files in the right order when linking, especially when they're not standard object files
# ar has really lame options, use -r to actually update stuff, -q is bogus

class Thing(object):
    def __str__(self):
        raise NotImplementedError("Things must override __str__")

class Command(object):
    def __init__(self, command, ignore=False, suppress=False):
        self.command = list2cmdline(command)
        self.ignore = ignore
        self.suppress = suppress
    def __str__(self):
        return ("@" if self.suppress else "") \
            + ("-" if self.ignore else "") \
            + self.command

class Recipe(object):
    def __init__(self, commands):
        self.commands = [c if isinstance(c, Command) else Command(c) for c in commands]
    def __str__(self):
        if self.commands:
            return "\t" + "\n\t".join(map(str, self.commands)) + "\n"
        else:
            return ""

class Include(Thing):
    def __init__(self, includes, ignore=False):
        self.includes = includes
        self.ignore = ignore
    def __str__(self):
        return ("-" if self.ignore else "") + "include " + self.includes + "\n"

class Rule(Thing):
    def __init__(self, targets, depends, recipe=()):
        self.targets = targets
        self.depends = depends
        self.recipe = Recipe(recipe)
    def __str__(self):
        return list2cmdline(self.targets) + " : " + list2cmdline(self.depends) + "\n" + str(self.recipe)

class Makefile(object):
    def __init__(self):
        """default is the name of the default action. clean is the name of the clean action, pass None if you don't want one."""
        self._things = []
        self._dirs = set()
        self._default = None
        self._phony = ["all", "clean"]
        self._setifndef = OrderedDict()
    def write_makefile(self, path="Makefile"):
        f = open(path, "w")
        f.write("MAKEFLAGS = -r -R --warn-undefined-variables\n")
        f.write(".PHONY : " + list2cmdline(self._phony) + "\n")
        f.write(".SUFFIXES : \n")
        f.write("\n")
        def write_thing(t):
            f.write(str(t))
            f.write("\n")
        for varname, value in self._setifndef.iteritems():
            write_thing(varname + " ?= " + value)
        write_thing("DIRS = " + list2cmdline(self._dirs))
        f.write("\n")
        write_thing(Rule(["all"], ["$(DIRS)"] + (self._default or list(chain(r.targets for r in self._things if isinstance(r, Rule))))))
        self(   ["clean"],
                [],
                [   Command(
                        RM + [r.targets for r in self._things if isinstance(r, Rule)],
                        ignore=True),
                    Command(
                        RMDIR + list(self._dirs),
                        ignore=True)],
                phony=True)
        write_thing(Rule(["$(DIRS)"], [], [Command(MAKEDIRS + ["$@"], ignore=True)]))
        #write_thing(self._things.pop("all"))
        for t in self._things:
            write_thing(t)
        f.close()
    def add_rule(self, *args, **kwargs):
        phony = kwargs.pop("phony", False)
        rule = Rule(*args, **kwargs)
        self._things.append(rule)
        if phony:
            self._phony.append(rule.targets)
        else:
            for t in rule.targets:
                dn = dirname(t)
                if dn:
                    self._dirs.add(dn)
    def __call__(self, *args, **kwargs):
        return self.add_rule(*args, **kwargs)
    def include(self, *args, **kwargs):
        self._things.append(Include(*args, **kwargs))
    def setifndef(self, varname, value):
        assert varname not in self._setifndef
        self._setifndef[varname] = value
    def default(self, depends):
        assert self._default is None
        self._default = depends

def pkg_config(options, packages):
    return split(Popen(["pkg-config"] + options + packages, stdout=PIPE).communicate()[0])

class Config(object):
    def __init__(self, initial):
        self.__values = initial
    def __getattr__(self, name):
        return self.__values[name]
    def __contains__(self, name):
        return name in self.__values
    def __setitem__(self, key, value):
        assert key in self.__values, key
        self.__values[key] = value
    def __getitem__(self, key):
        return self.__values[key]

def parse_args(**initial):
    from sys import argv
    from operator import iadd
    config = Config(initial)
    for a in argv[1:]:
        for s, op in [("+=", iadd)]:
            if s in a:
                k, v = a.split(s)
                op(config[k], eval(v))
                break
        else:
            if a.startswith("!"):
                config[a[1:]] = False
            elif a.startswith("no"):
                config[a[2:]] = False
            else:
                config[a] = True
    return config

if platform == "win32":
    MAKEDIRS = ["mkdir"]
    RM = ["del"]
    RMDIR = ["del"]
else:
    MAKEDIRS = ["mkdir", "-p"]
    RM = ["rm"]
    RMDIR = ["rmdir"]

conf = parse_args(
    build="$(BUILD)",
    mingw=False,
    checks=True,
    debug=True,
    optimize=False,
    windows=False,
    gglprof=False,
    CC=["cc"],
    CXX=["c++"],
    cppflags = [
            #"-DLOG_LEVEL_THRESHOLD=LOG_LEVEL_INFO",
            "-D_FILE_OFFSET_BITS=64",
            "-Iinclude",
            "-D_GNU_SOURCE",
        ],
    cxflags = [
            # the rest are for development
            "-Wextra",
            "-Wmissing-format-attribute",
            "-Wall",
            "-Wfloat-equal",
            "-fdiagnostics-show-option",
            "-Werror=return-type",
            #"-save-temps",
        ],
    cflags = [
            "-std=gnu99",
            "-x", "c",
            "-Wshadow", # broken for g++
            "-Werror-implicit-function-declaration",
        ],
    cxxflags = [
            "-std=gnu++0x",
            "-Woverloaded-virtual",
            "-Wno-invalid-offsetof",
        ],
    ldflags = [],)

if conf.mingw:
    conf.CC = ["gcc"]
    conf.CXX = ["g++"]

if not conf.checks:
    conf.cxflags += ["-DNDEBUG"]
if conf.debug:
    conf.cxflags += ["-g"]
if conf.optimize:
    conf.cxflags += ["-O3", "-march=native"]
    conf.ldflags += ["-O3", "-march=native"]
if conf.windows:
    conf.cxflags += [r"-IC:\Boost\include\boost-1_44"]
if conf.gglprof:
    conf.ldflags += ["-lprofiler"]

def cxmake(**kwargs):
    genobjs = []
    builddir = kwargs.pop("builddir", "$(BUILDDIR)")
    cxflags = kwargs.pop("cxflags", [])
    for src in kwargs["sources"]:
        obj = join(builddir, src + ".o")
        args = {
                "c": ["$(CC)"] + (kwargs.get("cflags", [])),
                "c++": ["$(CXX)"] + (kwargs.get("cxxflags", []))
            }[kwargs.get("language", None)
                or {True: "c", False: "c++"}[src.endswith(".c")]]
        args += cxflags + (kwargs.get("cppflags", []))
        for d in kwargs.pop("defines", []):
            args.append("-D" + d)
        args += ["-o", "$@", "-c", "$<"]
        mf([obj], [src], [args])
        genobjs.append(obj)
    kind = kwargs.pop("kind", "console")
    objects = kwargs.pop("objects", [])
    if kind is "static":
        target = "lib" + kwargs.pop("targetname") + ".a"
        mf([target], genobjs + objects, [
            ["ar", "-csur", target] + genobjs + objects])
    elif kind is "console":
        target = kwargs.pop("targetname")
        mf([target], genobjs + objects, [["$(CXX)", "-o", "$@"] + objects])
    else:
        assert False, "Unhandled kind: %r" % kind
    return target

mf = Makefile()
mf.setifndef("BUILDDIR", "mmbuild")
mf.setifndef("FUSE_CFLAGS", "`pkg-config --cflags fuse`")
mf.setifndef("FUSE_LIBS", "`pkg-config --libs fuse`")
#mf.
cpfs = cxmake(
    sources=iglob("cpfs/*.c*"),
    kind="static",
    targetname="cpfs")
mount_cpfs = cxmake(
    targetname="mount.cpfs",
    sources=iglob("mount_cpfs/*.c*"),
    objects=[cpfs],
    defines=["FUSE_USE_VERSION=28"],
    cflags=["$(CFLAGS)"],
    cxflags=["$(CXFLAGS)", "$(FUSE_CFLAGS)"],
    ldflags=pkg_config(["--libs"], ["fuse"]))
mkfs_cpfs = cxmake(
    targetname="mkfs.cpfs",
    sources=iglob("mkfs_cpfs/*.c*"),)
mf.default([mount_cpfs, mkfs_cpfs])
mf.write_makefile()
